iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0

DAY 10 完成專案啟動與狀態保存。今天先整理五個角色共用的模型呼叫流程,還不會把 Agent 接進 Discord 會議。

PM、Research、Creative、Finance 與 Review 的工作不同,但都需要準備 System Prompt、呼叫 Ollama、處理逾時與解析回覆。這些共同行為會放進 BaseAgent。

今天完成的內容

  • 建立 AgentConfig 保存角色與模型設定
  • 建立共用的 BaseAgent.respond()
  • 擴充 LLMService.chat()
  • 支援生成參數與 JSON Schema
  • 統一處理輸入、逾時與 LLM 錯誤
  • 使用 Fake LLM Service 測試

模型與 Agent 的差別

專案的五個角色共用 qwen3.5:4b。模型負責產生文字,Agent 則補上身份、任務規則與輸出要求。

使用者輸入
    ↓
Agent 名稱、角色、System Prompt
    ↓
生成設定與 JSON Schema
    ↓
LLMService → Ollama / qwen3.5:4b

不同 System Prompt 會讓同一個模型從不同角度回答,但模型本身沒有改變。

使用 AgentConfig 保存設定

@dataclass(frozen=True)
class AgentConfig:
    name: str
    role: str
    system_prompt: str
    timeout_seconds: float = 60.0
    temperature: float = 0.2
    seed: int = 42
    max_output_tokens: int = 500
    json_schema: Mapping[str, object] | None = None

name、role 與 system_prompt 定義身份;temperature、seed 和輸出上限控制生成;timeout_seconds 與 json_schema 負責執行限制和格式。

__post_init__() 會拒絕空白身份、非正數逾時與錯誤的 Token 上限,讓問題在建立 Agent 時就被發現。

BaseAgent 依賴的最小介面

程式使用 Protocol 描述需要的 chat():

class AgentLLMServiceProtocol(Protocol):
    async def chat(
        self,
        message: str,
        *,
        system_prompt: str | None = None,
        temperature: float | None = None,
        seed: int | None = None,
        max_output_tokens: int | None = None,
        json_schema: Mapping[str, object] | None = None,
    ) -> LLMResponse:
        ...

正式執行傳入 LLMService,單元測試則換成 Fake Service。只要提供相同的非同步 chat(),BaseAgent 就能使用。

組合 System Prompt

respond() 會先清除輸入前後空白,空字串直接拋出 AgentError。接著把名稱、角色及角色規則組在一起:

full_system_prompt = (
    f"Agent 名稱:{self.config.name}\n"
    f"角色:{self.config.role}\n\n"
    f"{self.config.system_prompt}"
)

這樣每個角色只要提供自己的設定,不必重寫共用提示詞格式。

呼叫 LLMService

response = await asyncio.wait_for(
    self.llm_service.chat(
        user_input,
        system_prompt=full_system_prompt,
        temperature=self.config.temperature,
        seed=self.config.seed,
        max_output_tokens=self.config.max_output_tokens,
        json_schema=self.config.json_schema,
    ),
    timeout=self.config.timeout_seconds,
)

Agent 逾時限制整次角色工作的等待時間;LLM Service 的 HTTP 逾時則負責 Ollama 請求。底層的 LLMServiceError 也會轉成 AgentError,讓後續流程只需處理一種 Agent 層級錯誤。

LLMService 接收 Agent 設定

有 System Prompt 時,它會排在 User Message 前面。生成參數則轉成 Ollama 的 options:

options = {
    "temperature": temperature,
    "seed": seed,
    "num_predict": max_output_tokens,
}

固定 Seed 可以降低相同輸入的變動,但模型版本或執行環境改變時,結果仍可能不同。

加入 JSON Schema

需要固定格式時,LLMService 會把 Schema 放入 Ollama 的 format:

if json_schema is not None:
    request["format"] = dict(json_schema)

模型回傳的 message.content 仍是 JSON 字串,因此 BaseAgent 會使用 json.loads() 解析,並確認最外層是物件或陣列。

try:
    data = json.loads(response.content)
except json.JSONDecodeError as error:
    raise AgentError("模型回覆不是合法 JSON。") from error

這一版只檢查 JSON 語法與最外層型別,還沒有在 Python 端逐欄驗證 Schema。Schema 也只能限制格式,不能保證內容正確。

AgentResponse 保存結果

@dataclass(frozen=True)
class AgentResponse:
    content: str
    data: dict[str, object] | list[object] | None
    usage: LLMUsage

content 是原始文字,data 是解析後的 JSON;沒有 Schema 時為 None。usage 沿用 LLM Service 的推論時間與 Token 統計。

使用 Fake LLM Service 測試

Fake Service 回傳固定內容並記錄呼叫參數。測試會確認名稱、角色、System Prompt、生成參數和 Schema 是否正確傳入,也會驗證 JSON 能否解析成 AgentResponse.data,過程不需要啟動 Ollama。

今天完成到哪裡?

Agent 現在能透過同一個 respond() 呼叫 LLM Service。角色設定集中在 AgentConfig,共用流程不用在五個 Agent 中重複實作。

這版尚未建立 PM、Research 等具體角色。下一篇才會加入第一批 Agent。


上一篇
DAY 10|完成 /start 與專案狀態
下一篇
DAY 12|建立 PM 與 Research Agent
系列文
AI 公司模擬器:Discord x Multi-Agent 架構實作 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言